import fs from "fs";
import path from "path";
import xlsx from "node-xlsx"
import { BinaryWriter, ObjCoder, StructValueType as VT, IStructField, BinaryWriteOp, StructCoder } from "k-coder";

/** Typescript 表vo对象构建 */
export default class TsXlsxVoBuilder {

    private vos: IXlsxVo[] = []
    private maps: IMap[] = []
    private exportTyDic: { [ty: string]: boolean };

    constructor(
        /** 表格目录 */
        readonly inputDir: string,
        /** 代码输出目录 */
        readonly codeOutputDir: string,
        /** 数据输出目录 */
        readonly dataOutputDir: string,
        /** 导出类型 空为全导 */
        readonly exportTy?: string,
        /** 输出数据文件名 */
        readonly dataName = "data",
        /** 输出文件名 */
        readonly fileName = "Xlsx",
        /** 输出Xlsx对象名 */
        readonly objName = "Xlsx",
        /** 输出XlsxVo名 */
        readonly voName = "XlsxVo",
        /** 输出自动生成的杂项代码名 */
        readonly miscName = "XlsxMisc",
    ) {
        if (exportTy) {
            this.exportTyDic = {};
            exportTy.split(",").forEach(ty => this.exportTyDic[ty] = true);
        }
    }

    /** 构建表格对象  */
    build() {
        this.loadAllXlsx();
        this.codeOutputDir && this.buildCode();
        this.dataOutputDir && this.buildData();
    }

    /** 读取所有表格 */
    private loadAllXlsx() {
        //遍历表格目录及其子目录读取表格
        (function eachDir(dir: string, fn: (filePath: string, fileName: string) => void) {
            fs.readdirSync(dir).forEach(fileName => {
                let filePath = path.resolve(dir, fileName);
                if (fs.statSync(path.resolve(dir, fileName)).isDirectory()) {
                    //递归遍历子目录
                    eachDir(filePath, fn);
                } else {
                    fn(filePath, fileName);
                }
            })
        })(this.inputDir, (filePath, fileName) => {
            let match = fileName.match(/^([^~].*)\.xlsx$/)
            if (match) {
                //检查命名是否规范
                console.log(filePath);
                assert(!match[1].match(/[^_0-9a-zA-Z]/), `[${fileName}]命名不规范`);
                let file = fs.readFileSync(filePath);
                this.loadXlsx(file, match[1], filePath);
            }
        })
        //根据表名排序下
        this.vos.sort((a, b) => a.name.localeCompare(b.name));
    }

    /** 读取单个表格 */
    private loadXlsx(file: Buffer, fileName: string, filePath: string) {
        let table = xlsx.parse(file);
        assert(table[0], `${fileName}`);
        let data: any[][] = table[0].data;
        let path = filePath.replace(this.inputDir, "").replace(/\\/g, "/").replace("/", "");
        let vo: IXlsxVo = {
            path,
            name: fileName,
            isCfg: data[0][0] == "#cfg",
            fields: [],
            data: [],
        }
        let maps = this.maps;
        if (vo.isCfg) {
            //将cfg类型转成与普通表格一致的结构再统一处理
            let temp: any[][] = [["导出类型："], ["数据类型："], ["字段名称："], ["字段注释："], ["*"]];
            let i = 0;
            while (data[++i]) {
                if (data[i][0] == "*") {
                    for (let j = 0; j < temp.length; j++) {
                        temp[j].push(data[i][j + 1]);
                    }
                }
            }
            data = temp;
        }

        for (let i = 4; i < data.length; i++) {
            //第一列有*号标识才导出
            data[i][0] == "*" && vo.data.push(data[i]);
        }
        //1-导出类型 2-数据类型 3-字段名称 4-字段注释
        let [exportTys, types, fields, notes] = data;

        //根据导出类型，筛选出需要导出的字段
        for (let i = 1; i < exportTys.length; i++) {
            let exportTy: string = exportTys[i];
            if (!exportTy || (this.exportTy && !exportTy.split(",").some(ty => this.exportTyDic[ty]))) continue;
            let type: string = types[i];
            let field: string = fields[i];
            let note = notes[i];
            if (field.match(/#(map|enum|const enum)/)) {
                parseMap(field, note, type);
            } else {
                parseField(i, note, field, type);
            }
        }

        /** 解析字段 */
        function parseField(col: number, note: string, name: string, type: string) {
            assert(!name.match(/[^_0-9a-zA-Z]\?+/), `[${fileName}.xlsx] >>> 字段<${name}>命名不规范`);
            let match = type.match(/([_0-9a-zA-Z]+)(?:\((\d+)\))*(?:\[(\d*)\])*/);
            let assertStr = `[${fileName}.xlsx] >>> ${note}：数据类型<${type}>解析错误`;
            assert(match, assertStr);
            let vt = VT[match[1]];
            assert(vt != undefined, assertStr);
            let arrVec = match[3] === "" ? 1 : match[3] ? parseInt(match[3]) : 0;
            let param = match[2] ? parseInt(match[2]) : 0;
            type = getTypeCode(vt, arrVec, assertStr);

            vo.fields.push({
                name: name.replace("?", ""),
                col,
                note,
                type,
                structField: [name, vt, arrVec, param]
            })
        }

        /** 解析映射 */
        function parseMap(mapType: string, note: string, type: string) {
            let match = type.match(/([_0-9a-zA-Z]+),([_0-9a-zA-Z]+)+,([_0-9a-zA-Z]+)+,([_0-9a-zA-Z]+)+/);
            assert(match, `[${fileName}.xlsx] >>> ${note}：${mapType}<${type}>解析错误`);
            let name = match[1];
            let key = match[2];
            let val = match[3];
            let fieldNote = match[4];
            let map: IMap = {
                mapType,
                path,
                note,
                name,
                data: vo.data,
                noteCol: -1,
                keyCol: -1,
                valCol: -1,
            }
            vo.fields.forEach(field => {
                switch (field.structField[0]) {
                    case key:
                        map.keyCol = field.col;
                        break;
                    case val:
                        map.valCol = field.col;
                        break;
                    case fieldNote:
                        map.noteCol = field.col;
                        break;
                }
            })
            assert(map.keyCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${key}>字段未找到`);
            assert(map.valCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${val}>字段未找到`);
            assert(map.noteCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${fieldNote}>字段未找到`);
            maps.push(map);
        }

        /** 获取值类型代码 */
        function getTypeCode(vt: VT, arrVec: number, assertStr: string) {
            let type = ""
            switch (vt) {
                case VT.bool:
                    type = "boolean";
                    break;
                case VT.uint:
                case VT.int:
                case VT.ulong:
                case VT.long:
                case VT.floatUint:
                case VT.floatInt:
                case VT.doubleUlong:
                case VT.doubleLong:
                case VT.doubleStr:
                    type = "number";
                    break;
                case VT.str:
                    type = "string";
                    break;
                case VT.obj:
                    type = "any";
                    break;
                case VT.arr:
                    type = "any[]";
                    break;
            }
            if (!type) {
                assert(type, assertStr);
            }
            while (arrVec--) {
                type += "[]";
            }
            return type;
        }
        this.vos.push(vo);
    }

    private buildCode() {
        let code = "//工具生成，请不要手动修改...\n";

        //写入kcoder初始化
        code += `let r: any, StructCoder: any;\n`;
        code += `/** 传入k-coder来进行初始化 */\n`;
        code += `export function initXlsx(kcoder: any) {\n`;
        code += `    r = new kcoder.BinaryReader();\n`;
        code += `    StructCoder = kcoder.StructCoder;\n`;
        code += `}\n\n`;

        //写入读取表数据方法
        code += `/** 表对象 */\n`;
        code += `export const ${this.objName} = {} as I${this.objName};\n`;
        code += `const dataDic: any = {};\n`;
        code += `const loadedXlsxDic: any = {};\n`;
        code += `const loadedCfgDic: any = {};\n`;
        code += `/** 读取表数据 */\n`;
        code += `export function loadData(buf: Uint8Array) {\n`;
        code += `    r.start(buf);\n`;
        code += `    while (r.hasNext) {\n`;
        code += `        let name = r.str;\n`;
        code += `        Object.defineProperty(${this.objName}, name, { get: r.byte ? () => loadCfg(name) : () => loadXlsx(name), enumerable: true, configurable: true });\n`;
        code += `        dataDic[name] = r.cut(r.uint);\n`;
        code += `    }\n`;
        code += `    for (let name in loadedXlsxDic) loadXlsx(name);\n`;
        code += `    for (let name in loadedCfgDic) loadCfg(name);\n`;
        code += `    r.end();\n`;
        code += `}\n\n`;

        code += `function loadVos(name: string) {\n`;
        code += `    r.start(dataDic[name]);\n`;
        code += `    delete dataDic[name];\n`;
        code += `    let struct: any = [];\n`;
        code += `    let len = r.uint;\n`;
        code += `    while (len--) struct.push([r.str, r.uint, r.uint, r.uint]);\n`;
        code += `    let coder = new StructCoder(struct);\n`;
        code += `    let vos: any = [];\n`;
        code += `    while (r.hasNext) vos.push(coder.decode(r));\n`;
        code += `    return vos;\n`;
        code += `}\n\n`;

        code += `function loadXlsx(name: string) {\n`;
        code += `    let vos = loadVos(name);\n`;
        code += `    let id2vo: any = {};\n`;
        code += `    for (let vo of vos) id2vo[vo.id] = vo;\n`;
        code += `    return loadedXlsxDic[name] = Object.defineProperty(${this.objName}, name, { value: { vos, count: vos.lengt, get: (id: number) => id2vo[id] } })[name];\n`;
        code += `}\n\n`;

        code += `function loadCfg(name: string) {\n`;
        code += `    return loadedCfgDic[name] = Object.defineProperty(${this.objName}, name, { value: loadVos(name)[0] })[name];\n`;
        code += `}\n\n`;

        //写入表对象
        code += `interface Xlsx<T> {\n`;
        code += `    readonly vos: T[];\n`;
        code += `    readonly count: number;\n`;
        code += `    get: (id: string | number) => T;\n`;
        code += `}\n\n`;

        code += `interface I${this.objName} {\n`;
        this.vos.forEach(vo => {
            let voName = `${this.voName}.${toFirstUpperCase(vo.name)}`;
            code += `    /** [${vo.path}] */\n`;
            code += `    readonly ${vo.name}: ${vo.isCfg ? voName : `Xlsx<${voName}>`};\n`;
        });
        code += `}\n\n`;

        //写入表vo对象
        code += `/** 表vo结构 */\n`
        code += `export namespace ${this.voName} {\n`
        this.vos.forEach(vo => {
            code += `\n    /** [${vo.path}] */\n`;
            code += `    export interface ${toFirstUpperCase(vo.name)} {\n`;
            vo.fields.forEach(field => {
                let { note, name, type } = field;
                if (note) code += `        /** ${note} */\n`;
                code += `        readonly ${name}: ${type}\n`;
            })
            code += `    }\n`;
        });
        code += `\n}\n`;

        //写入输出自动生成的杂项代码
        code += `/** 表自动生成的杂项代码 */\n`
        code += `export namespace ${this.miscName} {\n`
        this.maps.forEach(map => {
            switch (map.mapType) {
                case "#map":
                    code += `\n    const _${map.name} = {\n`;
                    map.data.forEach(row => {
                        let val = row[map.valCol];
                        code += `        /** ${row[map.noteCol]} */\n`;
                        code += `        ${row[map.keyCol]}: ${parseInt(val) != NaN ? `${val}` : `"${val}"`},\n`;
                    })
                    code += `    }\n`;
                    code += `    /** ${map.note}[${map.path}] */\n`;
                    code += `    export const ${map.name}:Readonly<typeof _${map.name}> = _${map.name}\n`;
                    break;
                case "#enum":
                case "#const enum":
                    code += `\n    /** ${map.note}[${map.path}] */\n`;
                    code += `    export ${map.mapType == "#const enum" ? "const " : ""}enum ${map.name} {\n`;
                    map.data.forEach(row => {
                        let val = row[map.valCol];
                        code += `        /** ${row[map.noteCol]} */\n`;
                        code += `        ${row[map.keyCol]} = ${parseInt(val) != NaN ? `${val}` : `"${val}"`},\n`;
                    })
                    code += `    }\n`;
                    break;
            }
        });
        code += `\n}`;

        fs.writeFileSync(path.resolve(this.codeOutputDir, `${this.fileName}.ts`), code, { encoding: "utf-8", flag: "w+" });
    }

    private buildData() {
        let w = new BinaryWriter();
        this.vos.forEach(vo => {
            let { name, fields, data } = vo;

            //[写入] >>> str-表名 byte-是否cfg
            w.str = name;
            w.byte = vo.isCfg ? 1 : 0;
            let startLen = w.len;
            let tableLenOp = w.pushOp(BinaryWriteOp.uint, 0, 0);

            //[写入] >>> uint-字段数量
            w.uint = fields.length;

            let struct: IStructField[] = [];
            fields.forEach(field => {
                let [fieldName, vt, arrVec, param] = field.structField;
                //[写入] >>> 数据结构 [str-字段名，uint-值类型 uint-数据维度 uint-float特殊参数]
                w.str = fieldName;
                w.uint = vt;
                w.uint = arrVec;
                w.uint = param as number;
                struct.push(field.structField);
            })
            let coder = new StructCoder(struct);

            data.forEach(dataRow => {
                let obj: any = {};
                fields.forEach(field => {
                    let val = dataRow[field.col];
                    if (val != undefined) {
                        obj[field.name] = (field.structField[2] > 0 || field.structField[1] >= VT.obj) ? JSON.parse(val) : val;
                    }
                });
                coder.encode(obj, w);
            })

            //[写入] >>> uint-表数据长度
            let tabelLen = w.len - startLen;
            tableLenOp.v = tabelLen;
            w.len += w.getUintLen(tabelLen);

        });

        //输出data文件
        fs.writeFileSync(path.resolve(this.dataOutputDir, `${this.dataName}.bin`), w.end(), { flag: "w+" });
    }
}

/** 首字母大写 */
function toFirstUpperCase(str: string) {
    return str[0].toUpperCase() + str.slice(1);
}

/** 断言失败退出程序并输出错误信息 */
function assert(cond: any, msg?: string) {
    if (!cond) {
        console.log(msg);
        process.exit();
    }
    return cond;
}

interface IField {
    name: string
    col: number,
    note: string,
    type: string,
    structField: IStructField
}

interface IXlsxVo {
    path: string,
    name: string,
    isCfg: boolean,
    fields: IField[],
    data: any[]
}

interface IMap {
    mapType: string,
    path: string,
    note: string,
    name: string,
    noteCol: number,
    keyCol: number,
    valCol: number
    data: any[],
}